# ===================================================================
#                     CameraVideoShooter 方法索引
# ===================================================================
# 在 PyCharm 中，您可以使用以下方式快速导航：
# 1. 使用 Ctrl+F 搜索方法名
# 2. 使用 Alt+7 查看结构视图（Structure）
# 3. 使用 Ctrl+Alt+Shift+N 搜索符号
#
# 基础操作:
#   - click_element: 点击元素
#   - get_element_by_text: 通过文本获取元素
#   - click_element_text: 点击文本元素
#   - input_text_in_element: 在元素中输入文本
#   - capture_screenshot: 截屏并保存
#   - random_input_chars: 随机输入字符
#
# 导航操作:
#   - enter_album: 进入相册
#   - enter_photograph: 进入拍摄页
#   - enter_photo: 进入拍照功能
#   - enter_photography_decoration_page: 视频拍摄后进入装饰页
#   - enter_pictures_decoration_page: 拍照后进入装饰页
#   - enter_the_production_step: 进入合拍流程
#
# 等待机制:
#   - wait_for_element_appearance: 等待元素出现
#   - wait_for_element_disappearance: 等待元素消失
#   - wait_for_image_appearance: 等待图片出现
#   - wait_for_text_not_equal: 等待文本不等于预期
#
# 本地相机操作:
#   - local_camera_takes_photos_hauweiP50: 华为拍照
#   - local_camera_takes_photos_vivoS7: vivo拍照
#   - set_fps_and_resolution: 设置帧率和分辨率
#   - local_camera_captures_videos_huaweiP50: 华为拍视频
#   - set_and_capture_video_vivoS7: vivo拍视频
#
# 断言验证:
#   - assert_element_exists: 断言元素存在
#   - assert_element_not_exists: 断言元素不存在
#   - assert_image_exists: 断言图片存在
#   - assert_image_not_exists: 断言图片不存在
#   - assert_images_equal: 断言图片一致
#   - assert_images_not_equal: 断言图片不一致
#   - assert_texts_equal: 断言文本相等
#   - assert_texts_not_equal: 断言文本不等
#   - assert_compare: 断言数值比较
#   - assert_toast_popup: 断言Toast弹窗
#
# 元素操作:
#   - click_child_nodes: 循环点击子节点
#   - multiple_dense_clicks: 多次密集点击
#   - get_element_text: 获取元素文本
#   - get_element_name: 获取元素name属性
#   - delete_multiple_elements: 删除多个元素
#   - get_child_count: 获取子元素数量
#   - get_element_size: 获取元素尺寸
#   - get_phone_model: 获取手机型号
#   - click_and_hold_element: 点击并按住元素
#
# 滑动操作:
#   - swipe_screen: 滑动屏幕
#   - slide_element_to_position: 滑动元素到位置
#   - swipe_until_element_found: 滑动直到找到元素
#   - swipe_from_template_to_point: 从模板位置滑动到点
#
# UGC相机控制:
#   - ensure_back_camera: 确保摄像头状态
#    -close_levitated_sphere:关闭悬浮球
#
# ======================= 文件内容开始 ========================
from airtest.core.api import *
from elements import *
from device_elements import *
import logging
from logging_config import setup_logging  # 导入自定义日志配置函数
import os
import time
import config
import re
from airtest.core.api import Template, exists, snapshot
from airtest.core.error import TargetNotFoundError
from PIL import Image
import numpy as np
import string
import random

# ==================== 优化后的日志配置 ====================
# 初始化业务日志器（仅通过 setup_logging 配置一次）
logger = setup_logging()

# 关闭 Airtest 底层模块的 DEBUG 日志（保持原有逻辑）
logging.getLogger("airtest").setLevel(logging.INFO)
logging.getLogger("airtest.core").setLevel(logging.INFO)
logging.getLogger("airtest.core.android.adb").setLevel(logging.INFO)
# ========================================================


# 初始化业务 logger（使用全局配置，无需额外设置级别）
logger = setup_logging()  # 假设原 setup_logging 仅用于设置日志格式，不影响级别


class CameraVideoShooter:
    def __init__(self):
        """初始化时直接使用elements.py的全局poco实例"""
        pass

    # ==================== 基础操作 ====================
    def click_element(self, element, wait_time=0.5):
        """
        极简点击方法：仅负责点击已定位的元素
        :param element: elements.py中定义的Poco元素对象
        """
        try:
            logger.info(f"等待元素出现: {element}")  # INFO 级别（保留）
            self.wait_for_element_appearance(element)
            logger.info(f"尝试点击元素: {element}")  # INFO 级别（保留）
            element.click()
            logger.info(f"元素 {element} 点击成功")  # INFO 级别（保留）
        except Exception as e:
            logger.error(f"【错误】元素点击失败 - 元素：{element}，原因：{str(e)}")  # ERROR 级别（保留）
            raise  # 抛出异常以便上层捕获

    def get_element_by_text(self, text):
        # 这里假设你使用的是poco库来获取元素，根据实际情况调整
        return poco(text=text)

    def click_element_text(self, element):
        if hasattr(element, 'click'):
            element.click()
            print(f"元素 {element} 点击成功")
        else:
            print(f"传入的不是可点击的对象")

    def input_text_in_element(self, element, text, wait_time=0.5):
        """
        在指定元素上输入文本
        :极简点击方法：仅负责点击已定位的元素
        :param element: elements.py中定义的Poco元素对象
        """
        try:
            logger.info(f"尝试在元素 {element} 上输入文本: {text}")
            element.set_text(text)
            logger.info(f"文本 {text} 已成功输入到元素 {element} 上")
            sleep(wait_time)
        except Exception as e:
            logger.error(f"【错误】在元素上输入文本失败 - 元素：{element}，文本：{text}，原因：{str(e)}")
            raise  # 抛出异常以便上层捕获

    def capture_screenshot(self, file_name="screenshot.png", save_dir=None):
        """
        封装截屏方法，将屏幕截图保存到指定路径（默认保存到 reports/case_screenshot）
        :param file_name: 截图保存的文件名，默认为 "screenshot.png"
        :param save_dir: 截图保存的目录，默认为 reports/case_screenshot
        :return: 截图文件的完整路径
        """
        # 如果未指定目录，使用全局配置的 case_screenshot 目录
        if save_dir is None:
            save_dir = config.CASE_SCREENSHOT_DIR
        # 确保目录存在（避免因配置未初始化导致的问题）
        os.makedirs(save_dir, exist_ok=True)
        # 拼接完整路径
        screenshot_path = os.path.join(str(save_dir), file_name)
        # 执行截图并等待
        snapshot(filename=screenshot_path)
        sleep(2.0)
        return screenshot_path

    def random_input_chars(self, input_element, char_count=25):
        """
        随机输入指定数量的字符（包含中英文大小写）到指定元素
        :param input_element: 要输入字符的元素对象
        :param char_count: 要输入的字符数量，默认为 25
        :return: 输入的随机字符串
        """
        try:
            # 定义所有可能的字符，包括大小写字母、中文字符
            all_chars = string.ascii_letters + ''.join([chr(i) for i in range(0x4e00, 0x9fff)])
            # 随机选择指定数量的字符
            random_chars = ''.join(random.choice(all_chars) for _ in range(char_count))
            logger.info(f"生成的随机字符串为: {random_chars}")
            # 输入随机字符串到指定元素
            input_element.set_text(random_chars)
            logger.info(f"成功将随机字符串输入到元素 {input_element}")
            return random_chars
        except Exception as e:
            logger.error(f"【错误】随机输入字符失败 - 元素：{input_element}，原因：{str(e)}")
            raise

    # ==================== 导航操作 ====================
    def enter_album(self):
        """封装进入相册的操作流程"""
        logger.info("【步骤】进入相册：点击UGC主版本入口")
        self.click_element(UGC_icon)  # 点击UGC主版本入口（来自elements.py的poco对象）

        logger.info("【步骤】进入相册：点击相册按钮")
        self.click_element(UGC_entrance)  # 点击相册按钮（来自elements.py的poco对象）

    def enter_photograph(self):
        """
        封装进入UGC相机的操作流程
        :return:
        """
        logger.info("【步骤】进入拍摄页：点击UGC主版本入口")
        self.click_element(UGC_icon)

        logger.info("【步骤】进入拍摄页：点击拍摄/相册 tab")
        self.wait_for_element_appearance(UGC_entrance, 10)
        sleep(1.0)
        self.click_element(UGC_entrance)

        logger.info("【步骤】进入拍摄页：点击拍摄")
        self.click_element(create_photograph)

    def enter_photo(self):
        """
        封装进入UGC相机拍照功能
        :return:
        """
        logger.info("【步骤】进入拍摄页：点击UGC主版本入口")
        self.click_element(UGC_icon)

        logger.info("【步骤】进入拍摄页：点击拍摄/相册 tab")
        self.click_element(UGC_entrance)

        logger.info("【步骤】进入拍摄页：点击拍摄")
        self.click_element(create_photograph)

        logger.info("【步骤】进入拍摄页：点击拍照")
        self.click_element(shoot_page_pictures)

    def enter_photography_decoration_page(self):
        """
        封装从拍摄页拍摄视频后进入装饰页的场景
        :return:
        """
        logger.info("【步骤】进入装饰页：进入拍摄")
        self.enter_photograph()
        logger.info("【步骤】进入装饰页：调整摄像头为前置状态")
        self.ensure_back_camera(is_back_camera=False)

        logger.info("【步骤】进入装饰页：拍摄视频")
        self.click_element(shooting_button)
        self.wait_for_element_appearance(shooting_page_next_button, 7)
        self.click_element(shooting_page_next_button)

    def enter_pictures_decoration_page(self):
        """
        封装从拍摄页拍照后进入装饰页的场景
        :return:
        """
        logger.info("【步骤】进入装饰页：进入拍摄")
        self.enter_photo()
        logger.info("【极简点击方法：仅负责点击已定位的元素步骤】进入装饰页：调整摄像头为前置状态")
        self.ensure_back_camera(is_back_camera=False)

        logger.info("【步骤】进入装饰页：拍照")
        self.click_element(take_pictures_button)
        sleep(5.0)

    def enter_the_production_step(self):
        """
        封装进入拍摄页合拍的流程
        """
        logger.info("【步骤一】进入相机拍摄")
        self.enter_photograph()

        logger.info("【步骤三】进入合拍功能：点击合拍按钮")
        self.click_element(shooting_page_in_time)

        logger.info("【步骤四】进入合拍功能：点击歌曲一")
        self.click_element(in_time_song_one)

    # ==================== 等待机制 ====================
    def wait_for_element_appearance(self, element_poco, timeout=10):
        """
        等待指定元素出现
        :param element_poco: 要等待出现的元素的poco对象
        :param timeout: 等待超时时间，默认 10 秒
        :return: 如果元素在超时时间内出现，返回该元素的poco对象；否则返回None
        """
        try:
            start_time = time.time()
            target_element = element_poco
            logger.info(f"开始等待元素出现，超时时间为 {timeout} 秒")
            target_element.wait_for_appearance(timeout=timeout)
            end_time = time.time()
            elapsed_time = end_time - start_time
            logger.info(f"目标元素已出现，等待时间为 {elapsed_time} 秒，继续执行后续代码")
            return target_element
        except Exception as e:
            end_time = time.time()
            elapsed_time = end_time - start_time
            logger.error(f"等待目标元素出现超时，等待时间为 {elapsed_time} 秒: {e}")
            return None

    def wait_for_element_disappearance(self, element_poco, timeout=10):
        """
        等待指定元素消失
        :param element_poco: 要等待消失的元素的poco对象
        :param timeout: 等待超时时间，默认 10 秒
        :return: 如果元素在超时时间内消失，返回 True；否则返回 False
        """
        if not element_poco.exists():
            logger.info("元素不存在，视为已消失")
            return True
        try:
            start_time = time.time()
            logger.info(f"开始等待元素消失，超时时间为 {timeout} 秒")
            element_poco.wait_for_disappearance(timeout=timeout)
            end_time = time.time()
            elapsed_time = end_time - start_time
            logger.info(f"目标元素已消失，等待时间为 {elapsed_time} 秒，继续执行后续代码")
            return True
        except Exception as e:
            end_time = time.time()
            elapsed_time = end_time - start_time
            logger.error(f"等待目标元素消失超时，等待时间为 {elapsed_time} 秒: {e}")
            return False

    def wait_for_image_appearance(self, image_path, record_pos=(0.011, -0.003), resolution=(1224, 2700), timeout=10):
        """
        等待指定图片出现
        :param image_path: 图片的路径
        :param record_pos: 图片记录位置，默认为(0.011, -0.003)
        :param resolution: 图片分辨率，默认为(1224, 2700)
        :param timeout: 等待超时时间，默认 10 秒
        :return: 如果图片在超时时间内出现，返回图片的位置；否则返回None
        """
        try:
            start_time = time.time()
            template = Template(image_path, record_pos=record_pos, resolution=resolution)
            logger.info(f"开始等待图片 {image_path} 出现，超时时间为 {timeout} 秒")
            pos = wait(template, timeout=timeout)
            end_time = time.time()
            elapsed_time = end_time - start_time
            logger.info(f"目标图片 {image_path} 已出现，等待时间为 {elapsed_time} 秒，继续执行后续代码")
            return pos
        except TargetNotFoundError:
            end_time = time.time()
            elapsed_time = end_time - start_time
            logger.error(f"等待目标图片 {image_path} 出现超时，等待时间为 {elapsed_time} 秒")
            return None

    def wait_for_text_not_equal(self, element, expected_text, timeout=10, interval=0.5):
        """
        等待指定元素的文本信息不为传入的预期文本
        :param element: 要检查的元素
        :param expected_text: 预期的文本信息
        :param timeout: 等待超时时间，默认 10 秒
        :param interval: 每次检查的时间间隔，默认 0.5 秒
        :return: 如果在超时时间内元素的文本信息不为预期文本，返回该元素的文本；否则返回 None
        """
        start_time = time.time()
        while time.time() - start_time < timeout:
            text = self.get_element_text(element)
            if text is not None and text != expected_text:
                logger.info(f"元素的文本信息不为 {expected_text}，继续执行后续代码")
                return text
            sleep(interval)
        logger.error(f"等待元素的文本信息不为 {expected_text} 超时")
        return None

    # ==================== 本地相机操作 ====================
    def local_camera_takes_photos_hauweiP50(self):
        """
        huawei调用本地相机拍摄照片
        :return:
        """
        logger.info("【操作】进入相机拍照")
        self.click_element(Machine_icon)
        self.click_element(local_photo_button)
        logger.info("【操作】拍摄照片")
        self.click_element(shutter_button)
        logger.info("【操作】关闭相机应用")
        stop_app("com.huawei.camera")

    def local_camera_takes_photos_vivoS7(self):
        """
        vivo调用本地相机拍摄照片
        :return:
        """
        logger.info("【操作】进入相机拍照")
        self.click_element(Machine_icon_vivo)
        self.click_element(local_photo_button_vivo)
        logger.info("【操作】拍摄照片")
        self.click_element(shutter_button_vivo)
        logger.info("【操作】关闭相机应用")
        stop_app("com.android.camera")

    def set_fps_and_resolution(self, fps_text, size_text):
        """
        封装设置相机帧率和分辨率的操作（内部方法）
        :param fps_text: 目标帧率（如"30 fps"）
        :param size_text: 目标分辨率（如"720p"")
        """
        # 直接使用elements.py中的poco元素对象获取文本
        # ------------------------ 设置帧率 ------------------------
        while True:
            current_fps = self.get_element_text(poco("com.huawei.camera:id/fps_show_text"))
            if current_fps == fps_text:
                break
            logger.info(f"【操作】点击帧率选项（当前={current_fps}）")
            self.click_element(fps_show_text)
            sleep(0.5)

        # ------------------------ 设置分辨率 ------------------------
        while True:
            current_size = self.get_element_text(poco("com.huawei.camera:id/size_show_text"))
            if current_size == size_text:
                break
            logger.info(f"【操作】点击分辨率选项（当前={current_size}）")
            self.click_element(size_show_text)
            sleep(0.5)

    def local_camera_captures_videos_huaweiP50(self, fps_text, size_text, duration, open_camera=True,
                                               close_camera=True):
        """
        使用本地相机拍摄视频的功能函数。
        参数:
        fps_text (str): 目标帧率，例如 "30 fps"，"60 fps"
        size_text (str): 目标分辨率，例如 "720p"，"4K"，"1080p"
        duration (float): 视频拍摄的时长，单位为秒。
        open_camera (bool, 可选): 是否打开相机应用。默认为 True
        close_camera (bool, 可选): 是否关闭相机应用。默认为 True
        返回:
        无
        """
        # 若 open_camera 为 True，则进入相机拍摄界面
        if open_camera:
            self.click_element(Machine_icon)
            self.click_element(Photograph)

        # 调用内部方法设置相机的帧率和分辨率
        self.set_fps_and_resolution(fps_text, size_text)
        # 等待 2 秒，确保设置生效
        sleep(2)

        # 再次检查相机当前的帧率和分辨率
        fps_show_element = poco("com.huawei.camera:id/fps极简点击方法：仅负责点击已定位的元素_show_text")
        if fps_show_element.exists():
            current_fps = fps_show_element.get_text()
        else:
            logger.error("无法找到 com.huawei.camera:id/fps_show_text 元素")
            current_fps = None

        size_show_element = poco("com.huawei.camera:id/size_show_text")
        if size_show_element.exists():
            current_size = size_show_element.get_text()
        else:
            logger.error("无法找到 com.huawei.camera:id/size_show_text 元素")
            current_size = None

        # 若当前帧率或分辨率与目标值不符，发出警告信息
        if current_fps != fps_text or current_size != size_text:
            logger.warning(f"【警告】实际帧率或分辨率与目标不符，当前帧率：{current_fps}，当前分辨率：{current_size}")

        # 记录开始拍摄视频的日志信息
        logger.info(f"【操作】开始拍摄：时长={duration}秒")
        # 点击快门按钮开始拍摄视频
        self.click_element(shutter_button)
        # 等待指定的拍摄时长
        sleep(duration)
        # 记录停止拍摄视频的日志信息
        logger.info("【操作】停止拍摄：再次点击快门按钮")
        # 再次点击快门按钮停止拍摄视频
        self.click_element(shutter_button)

        # 若 close_camera 为 True，关闭相机应用
        if close_camera:
            logger.info("【操作】关闭相机应用")
            stop_app("com.huawei.camera")
            logger.info("【操作】关闭相机后等待三秒")
            sleep(3.0)

    def set_and_capture_video_vivoS7(self, fps_element, resolution_element, duration):
        """
        封装设置相机帧率、分辨率并拍摄视频的操作流程
        :param fps_element: 帧率元素，对应选择帧率的元素对象
        :param resolution_element: 分辨率元素，对应选择分辨率的元素对象
        :param duration: 拍摄时长，单位为秒
        """
        try:
            # 点击进入相机
            logger.info("【步骤】点击进入相机")
            self.click_element(Machine_icon_vivo)

            # 点击设置录像
            logger.info("【步骤】点击设置录像")
            self.click_element(Photograph_vivo)

            # 点击设置
            logger.info("【步骤】点击设置")
            self.click_element(Photograph_set_up_vivo)

            # 点击分辨率
            logger.info("【步骤】点击分辨率")
            self.click_element(size_show_text_vivo)

            # 选择指定分辨率
            logger.info(f"【步骤】选择分辨率: {resolution_element}")
            self.click_element(resolution_element)

            # 点击帧率
            logger.info("【步骤】点击帧率")
            self.click_element(fps_show_text_vivo)

            # 选择指定帧率
            logger.info(f"【步骤】选择帧率: {fps_element}")
            self.click_element(fps_element)

            # 点击返回
            logger.info("【步骤】点击返回")
            self.click_element(Photograph_set_up_back_vivo)

            # 点击拍摄按钮拍摄指定时长的视频
            logger.info(f"【步骤】开始拍摄视频，时长: {duration} 秒")
            self.click_element(shutter_button_vivo)
            sleep(duration)

            # 停止拍摄
            logger.info("【步骤】停止拍摄视频")
            self.click_element(shutter_button_vivo)

            # 关闭相机
            logger.info("【步骤】关闭相机")
            stop_app("com.android.camera")
        except Exception as e:
            logger.error(f"【错误】执行拍摄视频操作时出错，原因: {str(e)}")
            raise

    # ==================== 断言验证 ====================
    def assert_element_exists(self, element, message):
        """
        封装断言元素是否存在的逻辑，并记录日志
        :param element: 要检查的元素
        :param message: 断言失败时的提示信息
        """
        try:
            if not element.exists():
                raise AssertionError(message)
            else:
                logger.info(f"元素 {element} 断言通过，元素存在")
        except AssertionError as e:
            logger.error(f"断言失败: {e}")
            raise

    def assert_element_not_exists(self, element, message):
        """
        封装断言元素是否不存在的逻辑，并记录日志
        :param element: 要检查的元素
        :param message: 断言失败时的提示信息
        """
        try:
            if element.exists():
                raise AssertionError(message)
            else:
                logger.info(f"断言通过，元素 {element} 元素不存在")
        except AssertionError as e:
            logger.error(f"断言失败: {e}")
            raise

    def assert_image_exists(self, image_path, message, record_pos=(0.011, -0.003), resolution=(1224, 2700), timeout=10,
                            threshold=0.8):
        """
        封装断言图片是否存在的逻辑
        :param image_path: 图片的路径
        :param message: 断言失败时的提示信息
        :param record_pos: 图片记录位置，默认为(0.011, -0.003)
        :param resolution: 图片分辨率，默认为(1224, 2700)
        :param timeout: 等待图片出现的超极简点击方法：仅负责点击已定位的元素时时间，默认为 10 秒
        :param threshold: 图片匹配度阈值，默认 0.8（即 80%）
        """
        try:
            template = Template(image_path, record_pos=record_pos, resolution=resolution, threshold=threshold)
            wait(template, timeout=timeout)
            logger.info(f"断言通过，图片 {image_path} 存在，匹配度阈值为 {threshold * 100}%")
        except TargetNotFoundError:
            logger.error(f"断言失败: {message}，匹配度阈值为 {threshold * 100}%")
            raise AssertionError(message)

    def assert_image_not_exists(self, image_path, message, record_pos=(0.011, -0.003), resolution=(1224, 2700),
                                threshold=0.8, check_interval=0.5, check_times=1):
        """
        封装断言图片是否不存在的逻辑，若不存在则断言成功
        :param image_path: 图片的路径
        :param message: 断言失败时的提示信息（即图片存在时的提示）
        :param record_pos: 图片记录位置，默认为(0.011, -0.003)
        :param resolution: 图片分辨率，默认为(1224, 2700)
        :param threshold: 图片匹配度阈值，默认 0.8（即 80%）
        :param check_interval: 每次检查的时间间隔，单位为秒，默认为 0.5 秒
        :param check_times: 检查的总次数，默认为 5 次
        """
        try:
            template = Template(image_path, record_pos=record_pos, resolution=resolution, threshold=threshold)
            for _ in range(check_times):
                if exists(template):
                    raise AssertionError
                sleep(check_interval)
            logger.info(f"断言通过，图片 {image_path} 不存在")
        except AssertionError:
            logger.error(f"断言失败: {message}")
            raise

    def assert_images_equal(self, image1, image2, message, threshold=0.8, record_pos=(0.0, 0.0),
                            resolution=(1224, 2700)):
        """
        断言两张图片内容一致（基于Airtest模板匹配，兼容新旧版本）
        :param image1: 基准图片路径或Template对象（视为屏幕截图）
        :param image2: 待比对图片路径或Template对象（视为匹配模板）
        :param message: 断言失败时的必传提示信息（如："截图与预期图不一致"）
        :param threshold: 相似度阈值（0-1），默认0.8（80%匹配度视为一致）
        :param record_pos: 图片记录位置，默认(0.0, 0.0)
        :param resolution: 图片分辨率，默认(1224, 2700)
        """

        # 统一转换为Template对象（兼容路径或Template输入）
        def to_template(img):
            return img if isinstance(img, Template) else Template(
                img, threshold=threshold, record_pos=record_pos, resolution=resolution
            )

        try:
            # 加载基准图和目标图
            screen_template = to_template(image1)
            target_template = to_template(image2)

            # 读取基准图为numpy数组（模拟屏幕截图）
            screen_array = np.array(Image.open(screen_template.filename))

            # 执行模板匹配（返回值兼容：旧版本元组(pos, confidence) / 新版本字典）
            match_result = target_template.match_in(screen_array)
            confidence = self._parse_confidence(match_result)  # 解析匹配度

            # 记录日志（与assert_image_exists格式一致）
            logger.info(f"图片比对：阈值{threshold * 100:.1f}%，实际相似度{confidence * 100:.1f}%")

            # 核心断言
            if confidence < threshold:
                logger.error(f"断言失败: {message}，相似度未达阈值")
                raise AssertionError(message)
            logger.info("断言通过，两张图片内容一致")

        except FileNotFoundError:
            logger.error(f"断言失败: 图片路径不存在 - {image1} 或 {image2}")
            raise
        except Exception as e:
            logger.error(f"图片比对异常: {str(e)}")
            raise

    def assert_images_not_equal(self, image1, image2, message, threshold=0.8, record_pos=(0.0, 0.0),
                                resolution=(1224, 2700)):
        """
        断言两张图片内容不一致（基于Airtest模板匹配，兼容新旧版本）
        :param image1: 基准图片路径或Template对象（视为屏幕截图）
        :param image2: 待比对图片路径或Template对象（视为匹配模板）
        :param message: 断言失败时的必传提示信息（如："两张图片意外一致"）
        :param threshold: 相似度阈值（0-1），默认0.8（80%匹配度视为一致）
        :param record_pos: 图片记录位置，默认(0.0, 0.0)
        :param resolution: 图片分辨率，默认(1224, 2700)
        """

        def to_template(img):
            return img if isinstance(img, Template) else Template(
                img, threshold=threshold, record_pos=record_pos, resolution=resolution
            )

        try:
            screen_template = to_template(image1)
            target_template = to_template(image2)
            screen_array = np.array(Image.open(screen_template.filename))
            match_result = target_template.match_in(screen_array)
            confidence = self._parse_confidence(match_result)

            logger.info(
                f"图片比对：阈值{threshold * 100:.1f}%，实际相似极简点击方法：仅负责点击已定位的元素度{confidence * 100:.1f}%")

            if confidence >= threshold:
                logger.error(f"断言失败: {message}，相似度超过阈值")
                raise AssertionError(message)
            logger.info("断言通过，两张图片内容不一致")

        except FileNotFoundError:
            logger.error(f"断言失败: 极简点击方法：仅负责点击已定位的元素图片路径不存在 - {image1} 或 {image2}")
            raise
        except Exception as e:
            logger.error(f"图片比对异常: {str(e)}")
            raise

    def _parse_confidence(self, match_result):
        """解析匹配结果中的置信度（兼容元组/字典格式）"""
        if match_result is None:
            return 0.0
        elif isinstance(match_result, tuple):
            return match_result[1] if len(match_result) >= 2 else 0.0  # 旧版本元组格式(pos, confidence)
        elif isinstance(match_result, dict):
            return match_result.get("confidence", 0.0)  # 新版本字典格式
        else:
            return 0.0  # 未知格式默认0

    def assert_texts_equal(self, text1, text2, message):
        """
        封装文本比对的断言逻辑
        :param text1: 第一个要比对的文本
        :param text2: 第二个要比对的文本
        :param message: 断言失败时的提示信息
        """
        try:
            assert_equal(text1, text2, message)
            logger.info(f"断言通过：文本比对成功，text1: {text1}，text2: {text2}")
        except AssertionError as e:
            logger.error(f"断言失败：文本比对失败: {e}")
            raise

    def assert_texts_not_equal(self, text1, text2, message):
        """
        封装文本比对的断言逻辑，若文本不一致则用例通过，若一致则断言失败
        :param text1: 第一个要比对的文本
        :param text2: 第二个要比对的文本
        :param message: 断言失败时的提示信息
        """
        try:
            if text1 == text2:
                raise AssertionError(message)
            logger.info(f"断言通过：文本比对不一致，text1: {text1}，text2: {text2}")
        except AssertionError as e:
            logger.error(f"断言失败：文本比对一致: {e}")
            raise

    def assert_compare(self, value1, value2, compare_type, message):
        """
        断言两个值的大小关系（支持带单位的字符串，如 '7.0s'）
        日志格式与其他断言方法保持一致：
        - 成功：[INFO] 断言通过，实际比对大小通过，{num1} {compare_symbol} {num2} 成立
        - 失败：[ERROR] 断言失败，{message}，实际值 {num1} {compare_symbol} {极简点击方法：仅负责点击已定位的元素num2} 不成立
        """
        # 定义比较类型映射（符号+比较函数）
        COMPARE_MAP = {
            "gt": (">", lambda a, b: a > b),
            "lt": ("<", lambda a, b: a < b),
            "ge": (">=", lambda a, b: a >= b),
            "le": ("<=", lambda a, b: a <= b)
        }

        def extract_number(val):
            """从数值或带单位的字符串中提取数值"""
            if isinstance(val, (int, float)):
                return val
            if not isinstance(val, str):
                raise ValueError(f"类型错误：期望数值或字符串，实际为 {type(val).__name__}")
            match = re.search(r'\d+\.?\d*', val)  # 提取连续数字（含小数点）
            if not match:
                raise ValueError(f"无法从字符串 '{val}' 中提取有效数值")
            return float(match.group())

        try:
            # 提取数值（可能抛出ValueError）
            num1 = extract_number(value1)
            num2 = extract_number(value2)
        except Exception as e:
            logger.error(f"断言参数格式错误 - {str(e)}")
            raise AssertionError(f"断言失败 - 参数格式错误：{str(e)}")

        # 校验比较类型有效性
        if compare_type not in COMPARE_MAP:
            valid_types = ", ".join(COMPARE_MAP.keys())
            logger.error(f"不支持的比较类型 '{compare_type}'，仅支持：{valid_types}")
            raise ValueError(f"无效的比较类型，仅支持：{valid_types}")

        # 获取比较符号和函数
        compare_symbol, compare_func = COMPARE_MAP[compare_type]

        try:
            # 执行比较逻辑
            if not compare_func(num1, num2):
                raise AssertionError(f"{message}，实际值 {num1} {compare_symbol} {num2} 不成立")

            # 断言通过日志（使用全局logger，与其他方法格式一致）
            logger.info(f"断言通过，实际比对大小通过，{num1} {compare_symbol} {num2} 成立")

        except AssertionError as e:
            # 断言失败日志（使用全局logger，与其他方法格式一致）
            logger.error(f"断言失败 - {str(e)}")
            raise  # 保持断言行为，向上抛出异常

    def assert_toast_popup(self, click_element, image_name, message="未找到Toast弹窗", high_freq_interval=0.05,
                           high_freq_timeout=2, record_pos=(0.011, -0.003), resolution=(1224, 2700), threshold=0.7):
        """
        断言toast弹窗是否出现，优化点击与检测之间的延迟
        :param click_element: 要点击的元素
        :param image_name: 要检测的图片名称（包含后缀）
        :param message: 断言失败时的提示信息
        :param high_freq_interval: 高频检测的时间间隔（秒），默认0.05秒
        :param high_freq_timeout: 高频检测的超时时间（秒），默认3秒
        :param record_pos: 图片记录位置
        :param resolution: 图片分辨率
        :param threshold: 图片匹配度阈值，降低至0.7提高灵敏度
        """
        try:
            # 处理图片路径
            image_path = os.path.join(os.getcwd(), image_name)
            template = Template(image_path, record_pos=record_pos, resolution=resolution, threshold=threshold)

            # 使用无延迟的点击操作（直接调用底层API）
            logger.info(f"执行无延迟点击: {click_element}")
            if hasattr(click_element, 'click'):
                # 获取元素位置并直接点击（跳过Airtest默认的稳定时间）
                pos = click_element.get_position()
                touch(pos)
            else:
                # 如果是坐标点或其他类型，直接点击
                touch(click_element)

            # 立即开始高频检测
            start_time = time.time()
            logger.info(f"开始高频检测Toast弹窗，间隔{high_freq_interval}秒，超时{high_freq_timeout}秒")

            # 存储初始时间用于计算实际耗时
            actual_start_time = start_time

            while True:
                elapsed_time = time.time() - start_time
                if elapsed_time > high_freq_timeout:
                    error_msg = f"{message}，超时时间{high_freq_timeout}秒"
                    logger.error(error_msg)
                    assert False, error_msg

                # 尝试获取当前屏幕截图
                try:
                    screen = G.DEVICE.snapshot()
                    if screen is None:
                        logger.warning("获取屏幕截图失败，重试中...")
                        time.sleep(high_freq_interval)
                        continue
                except Exception as e:
                    logger.error(f"截图过程中发生异常: {e}")
                    time.sleep(high_freq_interval)
                    continue

                # 使用获取的截图进行模板匹配
                match_result = exists(template)

                if match_result:
                    actual_elapsed = time.time() - actual_start_time
                    logger.info(f"成功检测到Toast弹窗，耗时{actual_elapsed:.2f}秒")
                    return True

                # 短暂等待后继续检测
                time.sleep(high_freq_interval)

        except Exception as e:
            error_msg = f"断言toast弹窗时出错: {e}"
            logger.error(error_msg)
            assert False, error_msg

    # ==================== 元素操作 ====================
    def click_child_nodes(self, common_node, start_index, end_index, child_selector2=None):
        """
        循环点击指定范围内的子节点(选择多个素材使用的方法)
        :param common_node: 父节点元素对象
        :param start_index: 起始索引
        :param end_index: 结束索引（包含）
        :param child_selector2: 二级子节点选择器（可以为空）
        """
        for index in range(start_index, end_index + 1):
            try:
                if child_selector2:
                    target_node = common_node[index].child(child_selector2)
                else:
                    target_node = common_node[index]

                if target_node.exists():
                    target_node.click()
                    logger.info(f"成功点击索引为 {index} 的节点")
                else:
                    logger.info(f"未找到索引为 {index} 的节点")
            except Exception as e:
                logger.error(f"处理索引为 {index} 的节点时出错: {e}")

    def multiple_dense_clicks(self, element, click_count=20, interval=0.5):
        """
        封装多次点击的操作方法
        :param element: 要点击的元素对象
        :param click_count: 点击次数，默认 20 次
        :param interval: 每次点击之间的间隔时间，单位为秒，默认 0.5 秒
        :return: 无
        """
        try:
            for _ in range(click_count):
                logger.info(f"【操作】尝试点击元素: {element}")
                element.click()
                logger.info(f"【操作】元素 {element} 点击成功")
                sleep(interval)
        except Exception as e:
            logger.error(f"【错误】多次点击元素失败 - 元素：{element}，原因：{str(e)}")

    def get_element_text(self, element, index=None, child_selector1=None, child_selector2=None):
        """
        获取指定节点的文本信息
        :param element: 节点元素对象，可以是单个元素或父节点元素
        :param index: 父节点下的索引，默认为 None
        :param child_selector1: 一级子节点选择器，可空
        :param child_selector2: 二级子节点选择器，可空
        :return: 节点的文本信息
        """
        try:
            if index is None:
                target_node = element
            elif child_selector1 and child_selector2:
                target_node = element[index].child(child_selector1).child(child_selector2)
            elif child_selector1 and not child_selector2:
                target_node = element[index].child(child_selector1)
            elif not child_selector1 and child_selector2:
                target_node = element[index].child(child_selector2)
            else:
                target_node = element[index]

            text = target_node.get_text()
            logger.info(f"成功获取节点的文本信息: {text}")
            return text
        except IndexError:
            logger.error(f"索引 {index} 越界，无法获取节点的文本信息")
        except AttributeError:
            logger.error("目标节点不存在 get_text 方法")
        except Exception as e:
            logger.error(f"获取节点的文本信息时出现未知错误: {e}")
        return None

    def get_element_name(self, element, index=None, child_selector1=None, child_selector2=None):
        """
        获取指定节点的name属性信息
        :param element: 节点元素对象，可以是单个元素或父节点元素
        :param index: 父节点下的索引，默认为 None
        :param child_selector1: 一级子节点选择器，可空
        :param child_selector2: 二级子节点选择器，可空
        :return: 节点的name属性信息
        """
        try:
            if index is None:
                target_node = element
            elif child_selector1 and child_selector2:
                target_node = element[index].child(child_selector1).child(child_selector2)
            elif child_selector1 and not child_selector2:
                target_node = element[index].child(child_selector1)
            elif not child_selector1 and child_selector2:
                target_node = element[index].child(child_selector2)
            else:
                target_node = element[index]

            # 获取元素的name属性
            element_name = target_node.attr('name')
            logger.info(f"成功获取节点的name属性: {element_name}")
            return element_name
        except IndexError:
            logger.error(f"索引 {index} 越界，无法获取节点的name属性")
        except AttributeError:
            logger.error("目标节点不存在 name 属性")
        except Exception as e:
            logger.error(f"获取节点的name属性时出现未知错误: {e}")
        return None

    def delete_multiple_elements(self, base_elements, start_index, end_index, step, child_selector=None):
        """
        循环删除指定范围内元素的子元素
        :param base_elements: 基础元素列表
        :param start_index: 起始索引
        :param end_index: 结束索引
        :param step: 步长
        :param child_selector: 子元素选择器，可空
        """
        for i in range(start_index, end_index, step):
            try:
                if child_selector:
                    target_element = base_elements[i].child(child_selector)
                else:
                    target_element = base_elements[i]

                if target_element.exists():
                    target_element.click()
                    logger.info(f"成功点击索引为 {i} 的删除按钮")
                else:
                    logger.info(f"未找到索引为 {i} 的删除按钮")
            except IndexError:
                logger.error(f"索引 {i} 越界，无法找到对应的元素")
            except Exception as e:
                logger.error(f"处理索引为 {i} 的元素时出现未知错误: {e}")

    def get_child_count(self, poco_object):
        """
        获取指定poco对象的子元素数量
        :param poco_object: 要获取子元素数量的poco对象
        :return: 子元素的数量
        """
        try:
            # 获取子元素列表
            children = poco_object.children()
            # 计算子元素数量
            child_count = len(children)
            logger.info(f"成功获取 {poco_object} 的子元素数量: {child_count}")
            return child_count
        except Exception as e:
            logger.error(f"获取 {poco_object} 的子元素数量时出现错误: {e}")
            return None

    def get_element_size(self, element):
        """
        获取指定元素的长和宽
        :param element: 要获取尺寸的元素对象
        :return: 元素的长和宽，格式为 (width, height)
        """
        try:
            size = element.get_size()
            width = size[0]
            height = size[1]
            logger.info(f"成功获取元素的长和宽，长: {width}，宽: {height}")
            return width, height
        except Exception as e:
            logger.error(f"获取元素的长和宽时出现错误: {e}")
            return None

    def get_phone_model(self):
        """
        获取手机品牌和型号信息
        :return: 手机品牌和型号信息
        """
        try:
            brand = shell("getprop ro.product.brand").strip()
            full_model = f"{brand}"
            logger.info(f"获取到的手机品牌和型号为: {full_model}")
            return full_model
        except Exception as e:
            logger.error(f"【错误】获取手机品牌和型号时出错，原因: {str(e)}")
            return None

    # ==================== 滑动操作 ====================
    def swipe_screen(self, start_percent, end_percent, duration=None, times=1):
        """
        封装屏幕滑动操作，根据百分比计算起始点和结束点并进行滑动
        :param start_percent: 起始点的百分比坐标，格式为 (x_percent, y_percent)
        :param end_percent: 结束点的百分比坐标，格式为 (x_percent, y_percent)
        :param duration: 滑动持续时间，可空，默认为 1 秒
        :param times: 滑动的总次数，默认为 1 次
        :return: 滑动成功返回 True，失败返回 False
        """
        try:
            # 获取屏幕的尺寸
            screen_width, screen_height = poco.get_screen_size()
            logger.info(f"屏幕宽度: {screen_width}, 屏幕高度: {screen_height}")

            # 根据百分比计算起始点和结束点的实际坐标
            start_x = screen_width * start_percent[0]
            start_y = screen_height * start_percent[1]
            end_x = screen_width * end_percent[0]
            end_y = screen_height * end_percent[1]

            # 限制坐标在屏幕范围内（避免越界）
            start_x = max(0, min(screen_width, start_x))
            start_y = max(0, min(screen_height, start_y))
            end_x = max(0, min(screen_width, end_x))
            end_y = max(0, min(screen_height, end_y))

            logger.info(f"起始点坐标: ({start_x}, {start_y})，结束点坐标: ({end_x}, {end_y})")

            # 处理滑动持续时间
            duration = duration or 1  # 若 duration 为 None，默认 1 秒

            # 执行滑动（首次）
            swipe((start_x, start_y), (end_x, end_y), duration=duration)
            logger.info(f"成功从 {start_percent} 滑动到 {end_percent}，持续时间 {duration} 秒")

            # 执行多次滑动（若 times > 1）
            for _ in range(times - 1):
                swipe((start_x, start_y), (end_x, end_y), duration=duration)
                logger.info(f"成功从 {start_percent} 滑动到 {end_percent}，持续时间 {duration} 秒")

            return True  # 滑动成功返回 True

        except Exception as e:
            logger.error(f"滑动屏幕时出现错误: {e}")
            return False  # 滑动失败返回 False

    def slide_element_to_position(self, element_node, target_position):
        """
        封装滑动元素到指定位置的方法
        :param element_node: 要滑动的元素节点
        :param target_position: 目标位置，格式为元组 (x, y)，取值范围 0 - 1
        :return: 如果元素存在并成功滑动返回 True，否则返回 False
        """
        slider_elenment = element_node
        if slider_elenment.exists():
            slider_elenment.drag_to(target_position)
            return True
        else:
            print("滑动元素未找到。")
            return False

    def swipe_until_element_found(self, element, start_percent, end_percent, max_swipes=10):
        """
        若未找到元素，则持续滑动屏幕直至找到该元素或达到最大滑动次数
        :param element: 要查找的目标元素
        :param start_percent: 滑动起始点的百分比坐标，格式为 (x_percent, y_percent)
        :param end_percent: 滑动结束点的百分比坐标，格式为 (x_percent, y_percent)
        :param max_swipes: 最大滑动次数，默认 10 次
        :return: 若找到元素返回 True，否则返回 False
        """
        found = False
        for _ in range(max_swipes):
            if element.exists():
                found = True
                logger.info(f"找到了 {element} 元素")
                break
            self.swipe_screen(start_percent, end_percent)

        if not found:
            logger.info(f"达到最大尝试次数，仍未找到 {element} 元素")
        return found

    def swipe_from_template_to_point(self, template_path, record_pos=(-0.025, 0.452),
                                     resolution=(1224, 2700),
                                     target_pos=(0, 0)):
        """
        从指定模板图片位置滑动到指定坐标点
        :param template_path: 模板图片的路径
        :param record_pos: 模板图片记录位置，默认为(-0.025, 0.452)
        :param resolution: 模板图片分辨率，默认为(1224, 2700)
        :param target_pos: 目标点的坐标，格式为 (x, y)
        """
        try:
            template = Template(template_path, record_pos=record_pos, resolution=resolution)
            logger.info(f"尝试从模板图片 {template_path} 滑动到坐标点 {target_pos}")
            swipe(template, target_pos)
            logger.info(f"从模板图片 {template_path} 滑动到坐标点 {target_pos} 成功")
        except Exception as e:
            logger.error(
                f"【错误】从模板图片滑动到坐标点失败 - 模板图片：{template_path}，目标坐标：{target_pos}，原因：{str(e)}")
            raise  # 抛出异常以便上层捕获

    # ==================== 相机控制 ====================
    def ensure_back_camera(self, is_back_camera=True):
        """
        确保摄像头为指定状态（前置或后置）
        :param is_back_camera: 布尔值，True 表示后置摄像头，False 表示前置摄像头，默认为后置摄像头
        """
        if is_back_camera:
            # 确保为后置摄像头
            if shooting_page_flashlight_elenment.exists():
                logger.info("此时为后置摄像头状态")
            else:
                try:
                    self.click_element(shooting_page_rollover_Button)
                    logger.info("将摄像头切换为后置状态")
                except Exception as e:
                    logger.error(f"未知错误: {e}")
        else:
            # 确保为前置摄像头
            if not shooting_page_flashlight_elenment.exists():
                logger.info("此时为前置摄像头状态")
            else:
                try:
                    self.click_element(shooting_page_rollover_Button)
                    logger.info("将摄像头切换为前置状态")
                except Exception as e:
                    logger.error(f"未知错误: {e}")

    def click_and_hold_element(self, element, hold_time):
        """
        点击元素并按住不放指定时长
        :param element: elements.py中定义的Poco元素对象
        :param hold_time: 按住的时长，单位为秒
        :param kwargs: 其他可选参数
        """
        try:
            logger.info(f"尝试点击并按住元素: {element}，按住时长: {hold_time} 秒")
            element.long_click(duration=hold_time)
            logger.info(f"元素 {element} 点击并按住 {hold_time} 秒成功")
        except Exception as e:
            logger.error(f"【错误】元素点击并按住失败 - 元素：{element}，原因：{str(e)}")
            raise  # 抛出异常以便上层捕获

    def close_levitated_sphere(self):
        """
        关闭Demo悬浮球
        :return:
        """
        self.click_element(UGC_icon)
        self.click_element(demo_levitated_sphere)
        self.swipe_screen((0.5, 0.7), (0.5, 0.45))
        touch((0.5,0.85))